package com.tianji.learning.utils;

import cn.hutool.core.bean.BeanUtil;
import com.tianji.common.utils.JsonUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.learning.controller.service.ILearningLessonService;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.mapper.LearningRecordMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.DelayQueue;

@Slf4j
@RequiredArgsConstructor//自动注入构造函数的依赖项，实现@AutoWired功能
//用于处理学习记录的延迟任务
public class LearningRecordDelayTaskHandler {
    private final StringRedisTemplate redisTemplate;
    //DelayQueue表示是一个延迟队列，这个队列在元素到期时才能将元素取出来
    //DelayTask是一个泛型类，它封装了实际的任务数据，并实现了 Delayed 接口
    //RecordTaskData 是一个类，用于封装与学习记录相关的任务数据。它包含lessonId、sectionId和moment
    private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();
    private final static String RECORD_KEY_TEMPLATE = "learning:record:{}";
    private final LearningRecordMapper recordMapper;
    private final ILearningLessonService lessonService;
    private static volatile boolean begin = true;//静态方法，类加载时就有了
    //@PostConstruct注解的init方法在实例化(创建对象)后立即执行，用于启动一个异步任务处理延迟队列中的任务。
    @PostConstruct//PostConstruct意思就是构造函数执行前执行，先执行init()方法后执行构造方法
    public void init(){
        //启用一个异步线程执行handleDelayTask方法
        CompletableFuture.runAsync(this::handleDelayTask);
    }
    @PreDestroy//当前实例销毁之前该方法会执行
    public void destroy(){
        log.debug("关闭学习记录处理的延迟任务");
        begin = false;
    }
    //属于消费者进程，处理延迟队列中的任务
    //handleDelayTask方法的作用是在一个循环中不断从延迟队列中取出任务，处理学习记录，并在必要时更新数据库和缓存
    private void handleDelayTask(){
        while (begin){//循环从延迟队列中取出任务
            try {
                //1、从延迟队列中获取任务
                //延时队列中存放的是之前保存的数据
                //从延迟队列中获取任务
                DelayTask<RecordTaskData> take = queue.take();
                //获取任务中的数据
                RecordTaskData data = take.getData();
                //2、从redis中获取数据
                //redis中的数据是前端刚提交的数据，即学习记录数据
                LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
                if(record == null){
                    //数据还未保存到redis中
                    continue;
                }
                //3、比较最新的数据(redis中获取的)和之前保存的数据(延迟队列中的)的进度是否一样
                if(!record.getMoment().equals(data.getMoment())){
                    //进度不相等，说明视频还在继续播放，不用将播放记录保存到数据库里面
                    //直接跳过本次循环就行
                    continue;
                }
                //4、进度相同，说明视频已经停止播放了，需要将数据保存到数据库中
                //保存的数据是在延迟队列中获取的data数据
                //需要保存的数据库有两个，一个是学习进度表一个是课表
                //5、更新数据到学习进度表
                record.setFinished(null);
                recordMapper.updateById(record);
                //6、更新数据到课表里面
                //创建课表对象
                LearningLesson lesson = BeanUtil.copyProperties(data, LearningLesson.class);
                lesson.setLatestLearnTime(LocalDateTime.now());
                lessonService.updateById(lesson);
                log.debug("准备持久化学习记录信息");
            } catch (Exception e) {
                log.error("处理播放记录任务发生异常", e);
            }
        }
    }

    /**
     * 将学习记录任务添加到延迟队列里面
     * @param record
     */
    //属于生产者进程方法，将数据存入延迟队列和redis中，但二者过期时间不一样
    public void addLearningRecordTask(LearningRecord record){
        // 1.添加数据到Redis缓存
        writeRecordCache(record);
        // 2.提交延迟任务到延迟队列 DelayQueue
        queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));
    }

    /**
     * 将学习记录更新到redis里面
     * @param record
     */
    public void writeRecordCache(LearningRecord record) {
        log.debug("更新学习记录的缓存数据");
        try {
            // 1.数据转换
            String json = JsonUtils.toJsonStr(new RecordCacheData(record));
            // 2.写入Redis
            String key = StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());
            redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
            // 3.添加缓存过期时间
            redisTemplate.expire(key, Duration.ofMinutes(1));
        } catch (Exception e) {
            log.error("更新学习记录缓存异常", e);
        }
    }

    /**
     * 从redis中获取学习记录
     * @param lessonId
     * @param sectionId
     * @return
     */
    //学习记录表中记录的是对应的课表id(课表就包含了用户id和课程id信息)，小节id上的一些记录信息，包括进度，状态等
    public LearningRecord readRecordCache(Long lessonId, Long sectionId){
        try {
            // 1.读取Redis数据
            //根据课表id拼接redis的key
            String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
            //opsForHash().get方法，是操作redis的hash结构的数据的方法，它接受两个参数：键key和字段名sectionId.toString()
            //这个方法从 Redis 中的哈希表获取与给定键和字段名相关联的值，即学习记录(不过从redis中获取的对象都变成了Object)
            Object cacheData = redisTemplate.opsForHash().get(key, sectionId.toString());
            if (cacheData == null) {
                return null;
            }
            // 2.数据检查和转换
            //将Oject对象转化未LearningRecord对象
            return JsonUtils.toBean(cacheData.toString(), LearningRecord.class);
        } catch (Exception e) {
            log.error("缓存读取异常", e);
            return null;
        }
    }

    /**
     * 删除redis中的数据
     * @param lessonId
     * @param sectionId
     */
    public void cleanRecordCache(Long lessonId, Long sectionId){
        // 删除数据
        //拼接redis操作的key
        String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
        redisTemplate.opsForHash().delete(key, sectionId.toString());
    }

    /**
     * RecordCacheData 类用于封装学习记录的缓存数据
     */
    @Data
    @NoArgsConstructor//生成无参构造方法
    private static class RecordCacheData{
        private Long id;
        private Integer moment;
        private Boolean finished;

        public RecordCacheData(LearningRecord record) {
            this.id = record.getId();
            this.moment = record.getMoment();
            this.finished = record.getFinished();
        }
    }

    /**
     * RecordTaskData 类用于封装延迟任务所需的数据
     */
    @Data
    @NoArgsConstructor
    private static class RecordTaskData{
        private Long lessonId;
        private Long sectionId;
        private Integer moment;

        public RecordTaskData(LearningRecord record) {
            this.lessonId = record.getLessonId();
            this.sectionId = record.getSectionId();
            this.moment = record.getMoment();
        }
    }
}
